package server;


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.omg.CORBA.StringHolder;


public class ServerImpl {

	public static Repository inventory;
	CustomerFile files;
	String storeID;
	String nearest, secondNearest;
	private final ExecutorService threadPool;
	ClientUDP cobj1, cobj2;
	public static int portNumber;
	Future<Integer> receivedFromServer ;

	/**
	 * Constructor for DRsServiceServerImpl 
	 * @throws SocketException 
	 */
	public ServerImpl(String name) {
		storeID = name;
		inventory = new Repository();
		threadPool = Executors.newFixedThreadPool(30);
	}

	public void startServerSocket() throws SocketException{
		// start thread to wait at socket
		if(this.storeID.equals("Montreal")){
			threadPool.execute((Runnable) new ServerUDP(new DatagramSocket(60000), this));
		} else if(this.storeID.equals("Toronto")){
			threadPool.execute((Runnable) new ServerUDP(new DatagramSocket(60001), this));
		}else if(this.storeID.equals("Vancouver")){
			threadPool.execute((Runnable) new ServerUDP(new DatagramSocket(60002), this));
		}else {
			System.out.println("Unknown server ID");
		}
	}

	public void firstClientSocket() throws UnknownHostException, SocketException{
		if(this.storeID.equals("Montreal")){
			nearest = "Toronto"; secondNearest = "Vancouver";
			cobj1 = new ClientUDP(new DatagramSocket(), 60001);
		} else if (this.storeID.equals("Toronto")){
			nearest = "Montreal"; secondNearest = "Vancouver";
			cobj1 = new ClientUDP(new DatagramSocket(), 60000);
		} else if(this.storeID.equals("Vancouver")){
			nearest = "Toronto"; secondNearest = "Montreal";
			cobj1 = new ClientUDP(new DatagramSocket(), 60001);
		} else {
			System.out.println("Unknown server ID");
		}
	}

	public void secondClientSocket() throws UnknownHostException, SocketException{
		if(this.storeID.equals("Montreal")){
			cobj2 = new ClientUDP(new DatagramSocket(), 60002);
		} else if (this.storeID.equals("Toronto")){
			cobj2 = new ClientUDP(new DatagramSocket(), 60002);
		} else if(this.storeID.equals("Vancouver")){
			cobj2 = new ClientUDP(new DatagramSocket(), 60000);
		} else {
			System.out.println("Unknown server ID");
		}
	}

//	public void hello(StringHolder msg) {
//		System.out.println("Hello "+msg.value+ "!");
//		msg.value = "Welcome to DRS from "+this.storeID;
//	}



	public String buy(String customerID, String itemID, int numberOfItem) {
		int inStore = inventory.checkAndBuy(itemID, numberOfItem);
		int from1=0, from2=0;
		//System.out.println("inStore: "+inStore);
		if(inStore==numberOfItem){
			files = new CustomerFile(customerID);
			files.updateFile(itemID, numberOfItem);
			return Integer.toString(numberOfItem);

		} else {
			try {
				firstClientSocket();
			} catch (UnknownHostException e) {
				e.printStackTrace();
			} catch (SocketException e) {
				e.printStackTrace();
			}
			int requiredFromOthers = numberOfItem-inStore;
			try {
				from1 = sendServer(cobj1, itemID, requiredFromOthers);
			} catch (NumberFormatException e) {
				e.printStackTrace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (ExecutionException e) {
				e.printStackTrace();
			}
			if(from1==requiredFromOthers) {
				files = new CustomerFile(customerID);
				files.updateFile(itemID, numberOfItem);
				return Integer.toString(numberOfItem);
			} else {
				try {
					secondClientSocket();
				} catch (UnknownHostException e) {
					e.printStackTrace();
				} catch (SocketException e) {
					e.printStackTrace();
				}
				int req2 = requiredFromOthers-from1;
				try {
					from2 = sendServer(cobj2, itemID, req2);
				} catch (NumberFormatException e) {
					e.printStackTrace();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (ExecutionException e) {
					e.printStackTrace();
				}
				if(from2==req2){
					files = new CustomerFile(customerID);
					files.updateFile(itemID, numberOfItem);
					return Integer.toString(numberOfItem);
				} else
					return Integer.toString(inStore+from1+from2);
			}
		}
	}


	public String returnStock(String customerID, String itemID, int numberOfItem) {
		System.out.println("Before returning: "+inventory.getQuantity(itemID));
		inventory.addQuantity(itemID, numberOfItem);
		//update file
		files = new CustomerFile(customerID);
		files.updateFile(itemID, (-numberOfItem));
		System.out.println("After returning: "+inventory.getQuantity(itemID));
		return "1";
	}


	public String checkStock(String itemID) {
		int inStore=0, from1 = 0, from2 = 0;
		inStore = inventory.getQuantity(itemID);
		// start client socket
		try {
			firstClientSocket();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (SocketException e) {
			e.printStackTrace();
		}
		// call first server
		try {
			from1 = checkOtherServer(cobj1, itemID);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		//call second server
		try {
			secondClientSocket();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (SocketException e) {
			e.printStackTrace();
		}
		try {
			from2 = checkOtherServer(cobj2, itemID);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		//System.out.println(from1+"\t"+from2);
		int availability = inStore + from1 + from2;
		//System.out.println("returning in check: "+Integer.toString(availability));
		return Integer.toString(availability);
	}

	public String exchange(String customerID, String boughtItemID,
			int boughtNumber, String desiredItemID, int desiredNumber) {
		// check if customerID bought boughtNumber of boughtItemID
		int from1 = 0, from2 = 0;
		files = new CustomerFile(customerID);
		if(files.readFile(boughtItemID, boughtNumber)){
			if(inventory.exchange(boughtItemID, boughtNumber, desiredItemID, desiredNumber)){//if 2
				// process success in the same server
				files = new CustomerFile(customerID);
				//add to customer file
				files.updateFile(desiredItemID, desiredNumber);
				//subtract from customer file
				files.updateFile(boughtItemID, (-boughtNumber));
				return "1";
			} else{
				//ask nearest as (requiredFromOthers>0)
				int inStore = inventory.hold(customerID, desiredItemID, desiredNumber);
				int requiredFromOthers = desiredNumber-inStore;
				//System.out.println("wanted: "+requiredFromOthers);
				try {
					firstClientSocket();
				} catch (UnknownHostException e) {
					e.printStackTrace();
				} catch (SocketException e) {
					e.printStackTrace();
				}

				try {
					// check first nearest server
					from1 = suspend(cobj1, customerID, desiredItemID, requiredFromOthers);
				} catch (NumberFormatException e) {
					e.printStackTrace();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (ExecutionException e) {
					e.printStackTrace();
				}
				try {
					if(from1==requiredFromOthers){
						try {
							firstClientSocket();
						} catch (UnknownHostException e) {
							e.printStackTrace();
						} catch (SocketException e) {
							e.printStackTrace();
						}
						if(performExchange(cobj1,("commit "+customerID))) {//if 3
							// commit and buy
							inventory.remove(customerID);
							files = new CustomerFile(customerID);
							//add to customer file
							files.updateFile(desiredItemID, desiredNumber);
							//subtract from customer file
							files.updateFile(boughtItemID, (-boughtNumber));
							return "1";
						} else {
							rollBack(cobj1, null, customerID);
							return "0";
						}
					} else{
						// ask the next server
						try {
							secondClientSocket();
						} catch (UnknownHostException e) {
							e.printStackTrace();
						} catch (SocketException e) {
							e.printStackTrace();
						}
						int req2 = requiredFromOthers-from1;
						try {
							from2 = suspend(cobj2, customerID, desiredItemID, req2);
						} catch (NumberFormatException e) {
							e.printStackTrace();
						} catch (InterruptedException e) {
							e.printStackTrace();
						} catch (ExecutionException e) {
							e.printStackTrace();
						}
						try {
							try {
								firstClientSocket();
							} catch (UnknownHostException e) {
								e.printStackTrace();
							} catch (SocketException e) {
								e.printStackTrace();
							}try {
								secondClientSocket();
							} catch (UnknownHostException e) {
								e.printStackTrace();
							} catch (SocketException e) {
								e.printStackTrace();
							}
							if(from2==req2) {//if 4
								// if commit is true then buy
								if(performExchange(cobj1,("commit "+customerID)) && performExchange(cobj2, ("commit "+customerID))){
									inventory.remove(customerID);
									files = new CustomerFile(customerID);
									//add to customer file
									files.updateFile(desiredItemID, desiredNumber);
									//subtract from customer file
									files.updateFile(boughtItemID, (-boughtNumber));
									return "1";
								}else {
									rollBack(cobj1, cobj2, customerID);
									return "0";
								}
							} else {
								// item not available, so abort
								rollBack(cobj1, cobj2, customerID);
								return "0";
							}
						} catch (InterruptedException e) {
							e.printStackTrace();
						} catch (ExecutionException e) {
							e.printStackTrace();
						}
						// else 4
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (ExecutionException e) {
					e.printStackTrace();
				}
				//end of else 3
				// send abort info
				rollBack(cobj1, cobj2, customerID);
				return "0";
			}
			// else 2
		}else {
			return "2";
		}
		//else 1
	}

	public void shutdown() {
		// TODO Auto-generated method stub

	}

	void rollBack(ClientUDP obj1, ClientUDP obj2, String id){
		try {
			performExchange(obj1, ("abort "+id));
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		try {
			if(null!=obj2) 
				performExchange(obj2, ("abort "+id));
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}

	@SuppressWarnings("static-access")
	public byte[] provideService(byte[] request){
		byte[] in = new byte[16];
		byte[] out = new byte[16];
		in = request;
		//convert to string
		String fromserver = new String(in);
		//System.out.println("from server: "+fromserver);

		//split the string
		String[] result = fromserver.split("\\s");
//		for (int i=0;i<result.length;++i){
//			System.out.println("result at: "+i+" "+result[i]);
//		}

		// parse and call
		if(result[0].equals("check")) {
			String key = new String(request, 6, 4);
			//System.out.println("key: "+key);
			int temp = this.inventory.getQuantity(key);
			//System.out.println("temp int: "+ temp);
			//out = convertIntToBytes(temp);
			out = Integer.toString(temp).getBytes();
		} else if(result[0].equals("checkAndBuy")){
			String key = new String(request, 12, 4);
			String entire = new String(request);
			int n=0; //size of int in reply
			for (int i=17;i<entire.length();i++){
				if(Character.isDigit(entire.charAt(i))){
					n++;
				}
			}
			String sOfValue = new String(request, 17, n);
			//System.out.println("check and buy: "+key+"\t"+ sOfValue);
			int value = Integer.parseInt(sOfValue);
			//System.out.println("parseInt: "+value);
			int temp = this.inventory.checkAndBuy(key, value);
			out = Integer.toString(temp).getBytes();
		} else if(result[0].equals("commit")){
			String customer = new String(request, 7, 6);
			//System.out.println("commit: "+customer);
			if(inventory.remove(customer))
				out = Integer.toString(1).getBytes();
			else
				out = Integer.toString(0).getBytes();
		} else if(result[0].equals("abort")){
			String id = new String(request, 6, 6);
			//System.out.println("abort: "+id);
			boolean aborted = inventory.abort(id);
			if(aborted)
				out = Integer.toString(1).getBytes();
			else
				out = Integer.toString(0).getBytes();
		} else if(result[0].equals("suspend")){
			String id = new String(request, 8, 6);
			String key = new String(request, 15, 4);
			String entire = new String(request);
			int n=0; //size of int in reply
			for (int i=20;i<entire.length();i++){
				if(Character.isDigit(entire.charAt(i))){
					n++;
				}
			}
			String sOfValue = new String(request, 20, n);
			//System.out.println("hold: "+id+"\t"+key+"\t"+ sOfValue);
			int value = Integer.parseInt(sOfValue);
			//System.out.println("parseInt: "+value);
			int temp = this.inventory.hold(id, key, value);
			out = Integer.toString(temp).getBytes();
		}
		//System.out.println("out: "+out);
		return out;

	}

	public byte[] convertIntToBytes( final int i ) {
		ByteBuffer buff = ByteBuffer.allocate(4); // capacity of buffer
		// convert to int
		byte[] temp = buff.putInt(i).array();
		//System.out.println("converted: "+temp);
		return  temp;
	}

	int convertToInt(byte[] b){
		ByteBuffer buff = ByteBuffer.wrap(b);
		//System.out.println("converted: "+buff.getInt());
		return buff.getInt();
	}


	int sendServer(ClientUDP obj, String item, int number) throws NumberFormatException, InterruptedException, ExecutionException{
		byte[] request = new byte[16];
		request = ("checkAndBuy "+item+" "+number).getBytes();
		if(obj==cobj1){
		try {
			firstClientSocket();

		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (SocketException e) {
			e.printStackTrace(); 
		}
		}else{
			try {
				secondClientSocket();

			} catch (UnknownHostException e) {
				e.printStackTrace();
			} catch (SocketException e) {
				e.printStackTrace(); 
			}
		}
		obj.sendMessage = request;
		receivedFromServer = threadPool.submit(obj);
		//System.out.println("future in send: "+receivedFromServer.get());
		return receivedFromServer.get();
	}

	int checkOtherServer(ClientUDP obj, String item) throws NumberFormatException, InterruptedException, ExecutionException{
		obj.sendMessage = ("check "+item).getBytes();
		receivedFromServer = threadPool.submit(obj);
		//System.out.println("future in check: "+receivedFromServer.get());
		return  receivedFromServer.get();
	}

	boolean performExchange(ClientUDP obj, String task) throws InterruptedException, ExecutionException{
		//System.out.println("perform: "+task);
		obj.sendMessage = (task).getBytes();
		receivedFromServer = threadPool.submit(obj);
		//System.out.println("future in exchange: "+receivedFromServer.get());

		if(receivedFromServer.get()==0)
			return false;
		else 
			return true;
	}

	int suspend(ClientUDP obj, String id, String item, int number) throws InterruptedException, ExecutionException{
		obj.sendMessage = ("suspend "+id+" "+item+" "+number).getBytes();
		receivedFromServer = threadPool.submit(obj);
		//System.out.println("future in suspend: "+receivedFromServer.get());
		return receivedFromServer.get();
	}



//--------------------------------------------------------------------------------------------------------

class ClientUDP implements Callable<Integer> {

	DatagramSocket clientUDPSocket = null;
	InetAddress address = null;
	int port = 0;
	byte[] sendMessage;
	byte[] receivedMessage;


	public ClientUDP (DatagramSocket socket,  int portNo) throws UnknownHostException {
		clientUDPSocket = socket;
		address = InetAddress.getByName(null);
		sendMessage = new byte [16]; // client's message
		port = portNo; // server's port
		receivedMessage = new byte[16];
		//System.out.println("Connecting to server at: "+port);
	}

	@Override
	public Integer call() throws IOException {
		try {			
			// send request
			DatagramPacket requestToServer = new DatagramPacket(sendMessage, sendMessage.length, address, port);
			clientUDPSocket.send(requestToServer);

			// receive reply
			DatagramPacket replyFromServer = new DatagramPacket(receivedMessage, receivedMessage.length);
			clientUDPSocket.receive(replyFromServer);

			//System.out.println("Reply recd.: " + receivedMessage);
			int sizeOfString;
			String received = new String(receivedMessage, Charset.defaultCharset());
			sizeOfString = received.length();
			//System.out.println("size: "+sizeOfString);//gives the size of byte[]
			int n=0; //size of int in reply
			for (int i=0;i<sizeOfString;i++){
				if(Character.isDigit(received.charAt(i))){
					n++;
				}
			}
			//System.out.println("n: " + n);
			//getting message in the default character
			String reply = new String(receivedMessage, 0, n, Charset.defaultCharset());
			//System.out.println("Reply converted: " + reply);
			return Integer.parseInt(reply);

		}catch (SocketException e){
			System.out.println("Client Socket Exception: " + e.getMessage());
		}catch (IOException e){
			System.out.println("IOException: " + e.getMessage());
		}finally {
			if(clientUDPSocket != null) clientUDPSocket.close();
			//System.out.println("Closed client socket in finally!");
		}
		System.out.println("Exiting try catch in client!");
		return null;
	}
	
}


//-------------------------------------------------------------------------------------------------------------

class ServerUDP implements Runnable{
	// UDP Server
	DatagramSocket udpSocket = null;
	byte[] message, reply;
	ServerImpl sobject;

	public ServerUDP(DatagramSocket socket, ServerImpl obj) {
		udpSocket = socket;
		sobject = obj;
		System.out.println(sobject.storeID+" Server socket listening at: "+socket.getLocalPort());
	}

	@Override
	public void run() {

		try{
			
			while(true){
				message = new byte[256];
				reply = new byte[256];
				
				// receive client's request
				DatagramPacket clientRequest = new DatagramPacket(message, message.length);
				udpSocket.receive(clientRequest); 

				// parse requested data
				reply = sobject.provideService(message);
				//System.out.println("reply processed to bytes: " + reply);

				//send reply
				DatagramPacket replytoClient = new DatagramPacket(reply, reply.length, 
						clientRequest.getAddress(), clientRequest.getPort());
				udpSocket.send(replytoClient);
			}

		}catch(SocketException e){
			System.out.println("Server Socket Exception: " + e.getMessage());
		}catch (IOException e) {
			System.out.println("IO: " + e.getMessage());
		}finally {
			if(udpSocket != null) udpSocket.close();
		}
		System.out.println("Exiting server's try catch!");
	}

}

}